import asyncio
import threading
import logging
from typing import Optional

import paramiko
from .settings import read_settings


class SSHShellSession:
    def __init__(
        self,
        host: str,
        port: int,
        username: str,
        password: Optional[str],
        term: str = "xterm",
        width: int = 120,
        height: int = 32,
        timeout: int = 10,
    ) -> None:
        self._client = paramiko.SSHClient()
        self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self._host = host
        self._port = port
        self._username = username
        self._password = password
        self._timeout = timeout

        self._term = term
        self._width = width
        self._height = height

        self._channel: Optional[paramiko.Channel] = None
        self._reader_task: Optional[asyncio.Task] = None
        self._stop_event = threading.Event()
        self._logger = logging.getLogger(__name__)

    def connect(self) -> None:
        cfg = read_settings()
        keepalive = int(cfg.get("ssh_keepalive_seconds", 30))
        timeout = int(cfg.get("ssh_timeout_seconds", self._timeout))
        self._timeout = timeout
        self._logger.info(
            "[SSH] connect hostname=%s port=%s username=%s timeout=%s term=%s size=%sx%s",
            self._host,
            self._port,
            self._username,
            self._timeout,
            self._term,
            self._width,
            self._height,
        )
        self._client.connect(
            hostname=self._host,
            port=self._port,
            username=self._username,
            password=self._password,
            timeout=timeout,
            allow_agent=True,
            look_for_keys=True,
        )
        try:
            transport = self._client.get_transport()
            if transport is not None:
                # keepalive every 30s to avoid idle disconnects
                transport.set_keepalive(keepalive)
                self._logger.debug("[SSH] set keepalive=%ss", keepalive)
        except Exception:  # noqa: BLE001
            self._logger.exception("[SSH] failed to set keepalive")
        self._channel = self._client.invoke_shell(term=self._term, width=self._width, height=self._height)
        self._logger.info("[SSH] shell channel opened term=%s size=%sx%s", self._term, self._width, self._height)
        self._channel.settimeout(0.0)  # non-blocking

    async def write_stdin(self, data: bytes) -> None:
        if not self._channel:
            return
        self._logger.debug("[SSH] send stdin bytes=%s", len(data))
        await asyncio.to_thread(self._channel.send, data)

    async def resize_pty(self, width: int, height: int) -> None:
        if not self._channel:
            return
        self._width = width
        self._height = height
        self._logger.info("[SSH] resize pty -> %sx%s", width, height)
        await asyncio.to_thread(self._channel.resize_pty, width=width, height=height)

    async def iterate_stdout(self):
        if not self._channel:
            return

        while not self._stop_event.is_set():
            try:
                if self._channel.recv_ready():
                    data = await asyncio.to_thread(self._channel.recv, 4096)
                    if data:
                        self._logger.debug("[SSH] recv stdout bytes=%s", len(data))
                    if not data:
                        break
                    yield data
                else:
                    await asyncio.sleep(0.01)
            except Exception:  # noqa: BLE001
                self._logger.exception("[SSH] iterate_stdout error")
                break

    async def close(self) -> None:
        self._stop_event.set()
        if self._channel is not None:
            try:
                await asyncio.to_thread(self._channel.close)
                self._logger.info("[SSH] channel closed")
            except Exception:  # noqa: BLE001
                self._logger.exception("[SSH] channel close failed")
        try:
            await asyncio.to_thread(self._client.close)
            self._logger.info("[SSH] client closed")
        except Exception:  # noqa: BLE001
            self._logger.exception("[SSH] client close failed")
 